- Görkem Güray/
- SwiftUI in 100 Days Notes/
- Day 10 - Swift Struct - 1 : Struct, Computed Property and Property Observer/
Day 10 - Swift Struct - 1 : Struct, Computed Property and Property Observer
Table of Contents
How to Create a Struct? #
The struct in Swift allows us to create a comprehensive, custom data type with its own variables and its own functions.
A simple struct looks like the following;
struct Album {
let title: String
let artist: String
let year: Int
func printSummary() {
print("\(title) (\(year)) by \(artist)")
}
}
The code above creates a new type called Album
. This type contains two Strings, title
and artist
, an Int named year
and a function named printSummary()
.
When naming a Struct, the first letter is capitalized. Note that Album
in our example also starts with a capital letter.
Now we can create variables and constants of type Album, assign values to them or copy them. Just like we created String or Int before.
let red = Album(title: "Red", artist: "Taylor Swift", year: 2012)
let wings = Album(title: "Wings", artist: "BTS", year: 2016)
print(red.title)
print(wings.artist)
red.printSummary()
wings.printSummary()
//OUTPUT:
//----------------------------------------
//Red
//BTS
//Red (2012) by Taylor Swift
//Wings (2016) by BTS
We can create new data of type Album
like calling a function. As you can see, although both red
and wings
are of type Album
, they are completely separate from each other.
We can see this when we call printSummary()
on each struct. When we call printSummary()
on red
and wing
based on the same struct, they return different values.
Where things differ is when we want to have values that can change. For example, we can create an Employee
struct that can take a vacation when needed.
struct Employee {
let name: String
var vacationRemaining: Int
func takeVacation(days: Int) {
if vacationRemaining > days {
vacationRemaining -= days
print("I'm going on vacation!")
print("Days remaining: \(vacationRemaining)")
} else {
print("Oops! There aren't enough days remaining.")
}
}
}
When we want to write the code above, Swift will not want to build the code.
Swift creates structs and their data as constants. This makes Swift run faster. But when a function in a struct wants to change some data, it is not allowed to do so, because all the data is immutable.
So, if we have a function that modifies the data of struct, it should be marked with mutating
. If we have a function that does not modify the data, but only reads it, it can do its job without any problem and does not need to be marked with mutating
.
mutating func takeVacation(days: Int) {
After making the above change in our Struct code, it will work just fine. Let’s create a constant from Employee
struct:
let archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.takeVacation(days: 5)
print(archer.vacationRemaining)
When we want to generate the above code, we will get an error again. Because when we define a struct with a mutating
function as a constant, the data cannot be changed.
The valid code should be as follows.
var archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.takeVacation(days: 5)
print(archer.vacationRemaining)
Important points about Struct;
- Constants and variables of a Struct are called property.
- Functions belonging to a struct are called method.
- When a constant or variable is created from a struct, it is called an instance.
- When creating an instance from a struct, we use an initializer like the following.
Album(title: "Wings", artist: "BTS", year: 2016)
When creating an instance from a struct, we actually use the init
function. But since Swift does this for us, we don’t create an instance by typing init
. This is called syntactic sugar. Both of the following code examples do the same thing, and create instance from the Employee
struct.
var archer1 = Employee(name: "Sterling Archer", vacationRemaining: 14)
var archer2 = Employee.init(name: "Sterling Archer", vacationRemaining: 14)
The init()
function is created automatically according to the property we set when creating the struct.
For example, we have 2 property in our struct;
let name: String
var vacationRemaining = 14
Swift defaults to 14 for vacationRemaining
if we don’t specify it during init.
let kane = Employee(name: "Lana Kane")
let poovey = Employee(name: "Pam Poovey", vacationRemaining: 35)
But there is an important point to note here: If we define any struct property as a constant (let
) and assign a value to it, this property will not appear in the init function. (Because a constant can only be assigned a value once.) In order to use default value assignment, we must use a variable (var
).
Struct Computed Property #
Structs can have two kinds of property.
Stored Property : A variable (var
) or constant (let
) that holds a piece of data within an instance of the Struct
Computed Property : The value of the property is dynamically computed each time it is accessed. This makes computed property a mixture of stored property and function. It is accessed like stored property but works like a function.
Let’s use the simplified version of our earlier Employee
struct as our example.
struct Employee {
let name: String
var vacationRemaining: Int
}
var archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.vacationRemaining -= 5
print(archer.vacationRemaining)
archer.vacationRemaining -= 3
print(archer.vacationRemaining)
//OUTPUT:
//----------------------------------------
//9
//6
The above struct works, but we lose some valuable information. When we created the struct, we kept the number of vacation rights of the employee in the variable vacationRemainig
. But as the employee takes leave, we lose the information about the employee’s vacation rights.
We can overcome this problem by using computed property.
struct Employee {
let name: String
var vacationAllocated = 14
var vacationTaken = 0
var vacationRemaining: Int {
vacationAllocated - vacationTaken
}
}
Now instead of assigning vacationRemaining
directly, we calculate vacationRemaining
by subtracting the leave used from the vacation right.
When we want to learn vacationRemaining
we can read it like a standard stored property.
var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
print(archer.vacationRemaining)
archer.vacationTaken += 4
print(archer.vacationRemaining)
//OUTPUT:
//----------------------------------------
//10
//6
This is a really powerful feature. It looks like a normal property but when we want to read it, calculations are done.
IMPORTANT NOTE : Constants (let
) cannot be computed property (Why? 😁 because constants can be assigned values only once).
But we can’t write data to the vacationRemaining
property, because we haven’t told Swift how to do that. To do that, we need to provide a getter and a setter in this property. Getter means the code that reads the value and Setter means the code that writes the value.
Let’s add getter and setter to our Employee
struct.
var vacationRemaining: Int {
get {
vacationAllocated - vacationTaken
}
set {
vacationAllocated = vacationTaken + newValue
}
}
get
and set
can be written as in the example above. The important thing here is newValue
. This is automatically provided to us by Swift and stores the value that the user wants to assign to property.
After providing getter and setter we can change vacationRemaining
as we want.
var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
archer.vacationRemaining = 5
print(archer.vacationAllocated)
//OUTPUT:
//----------------------------------------
//9
Property Observer #
A piece of code that runs when a property changes is called a property observer. Property observer can be in two ways: didSet
when the property changes, willSet
observer before the property changes.
To understand why we would need Property obsever, let’s consider a code like the one below;
struct Game {
var score = 0
}
var game = Game()
game.score += 10
print("Score is now \(game.score)")
game.score -= 3
print("Score is now \(game.score)")
game.score += 1
//OUTPUT:
//----------------------------------------
//Score is now 10
//Score is now 7
In the code above, the score
property is modified and after each change the current score is printed with print
. But there is a problem: nothing is printed after the last score change.
Let’s write the same code with property observer.
struct Game {
var score = 0 {
didSet {
print("Score is now \(score)")
}
}
}
var game = Game()
game.score += 10
game.score -= 3
game.score += 1
//OUTPUT:
//----------------------------------------
//Score is now 10
//Score is now 7
//Score is now 8
There is also the Swift provided constant oldValue
which can be used in didSet
. Of course there is also the constant newValue
which is provided automatically in willSet
.
struct App {
var contacts = [String]() {
willSet {
print("Current value is: \(contacts)")
print("New value will be: \(newValue)")
}
didSet {
print("There are now \(contacts.count) contacts.")
print("Old value was \(oldValue)")
}
}
}
var app = App()
app.contacts.append("Adrian E")
app.contacts.append("Allen W")
app.contacts.append("Ish S")
//OUTPUT:
//----------------------------------------
//Current value is: []
//New value will be: ["Adrian E"]
//There are now 1 contacts.
//Old value was []
//Current value is: ["Adrian E"]
//New value will be: ["Adrian E", "Allen W"]
//There are now 2 contacts.
//Old value was ["Adrian E"]
//Current value is: ["Adrian E", "Allen W"]
//New value will be: ["Adrian E", "Allen W", "Ish S"]
//There are now 3 contacts.
//Old value was ["Adrian E", "Allen W"]
You need to be careful when using didSet
and willSet
. Because putting too much work on the property observer can cause performance problems.
Note : Property observer is not used with constants (let
) (because the value of constants is set only once)
How to Create Struct Custom Initializer? #
Initiliazer are special methods designed to prepare a new struct instance to be used. We have already seen how they are automatically generated based on the property we put into the struct. But we can also create our own custom initializer as long as we follow the rule. Rule of thumb: All property must have a value at the end of the initializer.
Let’s look at the automatically generated initializer;
struct Player {
let name: String
let number: Int
}
let player = Player(name: "Megan R", number: 15)
Swift creates an initializer for a new Player
instance with two existing properties by default. In Swift, this is called a memberwise initializer.
We can also write the init
function ourselves.
struct Player {
let name: String
let number: Int
init(name: String, number: Int) {
self.name = name
self.number = number
}
}
The code above does the same thing as the code we just wrote. But here the initializer belongs to us and we can add functionality if we want.
Some things to be aware of;
- There is no
func
keyword. Syntactically it looks like a function, but Swift privileges the initializer here. - Although
init
creates a newPlayer
instance, initializers do not have a fixed return type. - We use the keyword
self
to distinguish betweenname
used in struct andname
used in init (self.name
is the property).
Of course, a custom initializer does not have to work like the automatically generated memberwise initializer. For example, the name
variable can be given by the user, while the number
variable can be randomly assigned.
struct Player {
let name: String
let number: Int
init(name: String) {
self.name = name
number = Int.random(in: 1...99)
}
}
let player = Player(name: "Megan R")
print(player.number)
All properties must have a value at the end of the initializer. If the variable number
was not assigned a value, the above code would throw an error.
We can add more than one initializer to the struct, and we can take advantage of features like external parameter name and default value. It is important to note that when we create a custom initializer, we lose access to the memberwise initializer that is automatically generated by Swift, unless otherwise specified.
Using Memberwise Initializer and Custom Initializer Together #
We mentioned that if we use a custom initializer, the memberwise initializer is deprecated. But in Swift we can create an exception to this.
To create the exception, we will use extension
.
struct Employee {
var name: String
var yearsActive = 0
}
extension Employee {
init() {
self.name = "Anonymous"
print("Creating an anonymous employee…")
}
}
// We can create roslin by specifying the name variable.
let roslin = Employee(name: "Laura Roslin")
// We can create the anon variable without specifying any variable.
// in this case the name variable will be Anonymous.
let anon = Employee()
You can also read this article in Turkish.
Bu yazıyı Türkçe olarak da okuyabilirsiniz.